Docly Developers
Knowledge base Best practice Recommended

Cache-busting bundles with assetUrl

In shortStatic files (images, CSS, JS) are served with a one-year max-age, so an unchanged bundle URL keeps serving stale JS/CSS to returning visitors long after you change the file. Wrap your own <script src> and <link href> in assetUrl() to append a ?v= stamp that changes whenever anything in the published folder or its app is saved. Never use assetUrl on dynamic or per-user output — ?v= URLs get max-age=1 year immutable, which would freeze the first dynamic response for everyone.
Applies to Hash templatesStatic filesCaching
  1. What you'll see

    A developer publishes an updated JS or CSS bundle, but visitors keep running the old version — for as long as their browser keeps the cached copy, which can be up to a year — because the bundle URL never changed and the browser saw no reason to refetch it. The symptom is not just wrong styling: an old JS bundle running against new HTML causes functional breakage, and it clears itself only when the browser cache expires or the user hard-refreshes.

  2. What's actually happening

    Browsers cache static assets aggressively. If the URL of a bundle stays the same across a publish, the browser treats the cached copy as current and does not refetch it. docly.assetUrl(path) solves this by appending ?v=<version> to the path, where the version is the publication's last-modified time (the newest of the published folder and its app) formatted yyyyMMddHHmmss. The stamp is site-wide, not per file: it changes whenever anything in the published folder or in its app is saved. There is no separate publish step — a save is live.

    Without it, a static file is not revalidated either. Images, CSS and JS files are served with Cache-Control: max-age of one year, so a changed file reaches a returning visitor only when the cached copy expires.

    The server backs this up: any response whose URL carries a ?v= query is served with Cache-Control: max-age=31536000, immutable (one year). So an unchanged bundle is never refetched unnecessarily, and a changed bundle gets a brand-new URL after the next save and is refetched immediately. Public HTML without ?v= is served no-cache instead, so it revalidates cheaply (a 304 via Last-Modified). External URLs (containing :// or starting with //) are returned unchanged, with no ?v=.

    A Cache-Control header you set yourself in headers.json (or the headers block of site.json) replaces this policy for pages and files. API responses always stay no-cache.

  3. What to do

    Wrap all of your own <script src> and <link href> references in docly.assetUrl(...):

    <script src="#docly.assetUrl('/assets/js/bundle.js')#"></script>
    <link href="#docly.assetUrl('/assets/css/site.css')#" rel="stylesheet">

    The bundle now gets ?v=<version>. A save means a new version, a new URL, and an immediate refetch by the browser. The server sets immutable, max-age=1 year on ?v= URLs, so unchanged bundles are never refetched unnecessarily — this also scores full marks on Google's "efficient cache policy" audit.

    ⚠ Does NOT work for your own dynamic output. assetUrl versions by save time (the publication's last-modified time), not by the actual content of a dynamically generated response. So do not use assetUrl on a URL that produces content which varies without a save:

    • output that depends on query parameters, the logged-in user, the current time, counters, or external sources
    • anything that must be fresh per request

    Why it is dangerous: a ?v= URL gets immutable, max-age=1 year. If the browser or CDN caches the first dynamic response, that same response is served to everyone for a whole year — stale content, and potentially one user's data leaking to others.

    What to do instead: serve dynamic or per-user content from a .js API endpoint under #/API/. API responses are always no-cache and must not be versioned with assetUrl. If you need other caching headers on a response, note that docly.setHeader() exists only in #/API/ scripts; a page or bundle gets its headers from headers.json. Fetch them from the browser at runtime.

    Rule of thumb:

    • Static bundle/asset (changes only when you save it) → use assetUrl.
    • Dynamic / per-user / per-request output → do not — serve it as a .js API (no-cache).
    • External URL (CDN etc.) → no — assetUrl leaves it unchanged anyway.

Last reviewed 17 Oct 2026